package io.gitee.chearnee.fw.datasecurity.mybatisplus.interceptor;


import com.baomidou.mybatisplus.core.MybatisParameterHandler;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import io.gitee.chearnee.fw.common.utils.ConstStringPool;
import io.gitee.chearnee.fw.datasecurity.core.annotation.CryptField;
import io.gitee.chearnee.fw.datasecurity.core.exception.DbCryptRuntimeException;
import io.gitee.chearnee.fw.datasecurity.core.executor.CryptExecutor;
import io.gitee.chearnee.fw.datasecurity.core.executor.CryptExecutorFactory;
import io.gitee.chearnee.fw.datasecurity.core.properties.DataSecurityDbCryptProperty;
import io.gitee.chearnee.fw.datasecurity.core.resolver.MethodCryptMetadata;
import io.gitee.chearnee.fw.datasecurity.core.resolver.MethodDecryptResolver;
import io.gitee.chearnee.fw.datasecurity.core.resolver.SimpleMethodDecryptResolver;
import io.gitee.chearnee.fw.datasecurity.core.util.DbCryptUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.BindingException;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ParamNameResolver;
import org.apache.ibatis.reflection.ReflectionException;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.util.*;
import java.util.function.Consumer;

/**
 * @author kam
 * @since 2021/4/9
 *
 * <p>
 * mybatisPlus加密插件
 * 1.确定需要加密参数的fieldList，通过线程副本传递下去
 * </p>
 */
@Slf4j
@Intercepts(value = {
        @Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class,
                        BoundSql.class}),
        @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class MybatisPlusCryptInterceptor implements Interceptor {


    /**
     * 数据库加密属性
     */
    private DataSecurityDbCryptProperty dataSecurityDbCryptProperty;

    /**
     * 唯一结果处理
     */
    private MethodDecryptResolver simpleResultDecryptResolver;

    /**
     * statement加密元数据缓存管理器
     */
    private StatementCryptMetadataCacheManager metadataCacheManager;

    /**
     * 预加密的参数上下文信息
     */
    private static final ThreadLocal<PreCodecMetadata> CODEC_METADATA_THREAD_LOCAL = new ThreadLocal<>();

    public MybatisPlusCryptInterceptor(DataSecurityDbCryptProperty dataSecurityDbCryptProperty, CryptExecutor cryptExecutor) {
        if (Objects.isNull(dataSecurityDbCryptProperty)) {
            throw new DbCryptRuntimeException("dbcryptProperties must not null");
        }
        this.dataSecurityDbCryptProperty = dataSecurityDbCryptProperty;
        this.simpleResultDecryptResolver = new SimpleMethodDecryptResolver();
        this.metadataCacheManager = new StatementCryptMetadataCacheManager();

        CryptExecutorFactory.registry(cryptExecutor);//注册executor
    }

    private MybatisPlusCryptInterceptor() {
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (!isSwitchCrypt()) {
            return invocation.proceed();
        }

        //根据拦截的参数来确定执行哪个处理
        if (invocation.getTarget() instanceof Executor) {
            Object[] args = invocation.getArgs();
            MappedStatement mappedStatement = (MappedStatement) args[0];
            //寻找需加密字段的标识列表 入参标识列表 和 出参标识标识 传递下去，在随后的处理器做加密处理
            PreCodecMetadata metadata = this.metadataCacheManager.getCachedStatementPreCodecMetadata(mappedStatement.getId());
            CODEC_METADATA_THREAD_LOCAL.set(metadata);
            Object result = invocation.proceed();
            DbCryptUtil.cleanKeyGenerateReference();
            //结果解密处理
            return this.decryptResult(result);
        }

        if (invocation.getTarget() instanceof ParameterHandler) {
            PreCodecMetadata metadata = CODEC_METADATA_THREAD_LOCAL.get();
            if (Objects.isNull(metadata)) {
                return invocation.proceed();
            }
            Object result = this.invokeSetParameter(invocation, metadata);//结果为空
            CODEC_METADATA_THREAD_LOCAL.remove();
            return result;
        }
        return null;//不会执行
    }

    private Object decryptResult(Object result) {
        return this.simpleResultDecryptResolver.processDecrypt(result);
    }

    /**
     * 执行setParameter
     *
     * @param invocation invocation
     * @param metadata   预加密的元数据
     * @return setParameter执行结果
     * @throws Exception 其他异常
     */
    @SuppressWarnings("rawtypes")
    private Object invokeSetParameter(Invocation invocation, PreCodecMetadata metadata) throws Exception {
        ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
        PreparedStatement ps = (PreparedStatement) invocation.getArgs()[0];

        InvocationCls invocationCls = buildInvokeInfo(parameterHandler);

        // 反射获取参数对象
        Field parameterField = invocationCls.parameterObjectField;
        final Object[] parameterObject = {parameterHandler.getParameterObject()};
        // 反射获取 BoundSql 对象，此对象包含生成的sql和sql的参数map映射
        Field boundSqlField = invocationCls.boundSqlField;

        BoundSql boundSql = (BoundSql) boundSqlField.get(parameterHandler);
        Field typeHandlerRegistryField = invocationCls.typeHandlerRegistryField;

        TypeHandlerRegistry typeHandlerRegistry = (TypeHandlerRegistry) typeHandlerRegistryField.get(parameterHandler);
        Field configurationField = invocationCls.configurationField;

        Configuration configuration = (Configuration) configurationField.get(parameterHandler);
        checkLikeSql(boundSql, parameterObject[0], typeHandlerRegistry, configuration, (oldObjects) -> {
            if (parameterObject[0] == null) {
                return;
            }
            if (Map.class.isAssignableFrom(parameterObject[0].getClass())) {
                //如果是paramMap 且ew参数不为空 按照mybatisPlus的wrapper解析处理
                Wrapper wrapper = null;
                if (parameterObject[0] instanceof MapperMethod.ParamMap) {
                    try {
                        MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) parameterObject[0];
                        wrapper = (Wrapper) paramMap.get(Constants.WRAPPER);
                    } catch (BindingException | ClassCastException ignored) {
                    }
                }
                if (Objects.nonNull(wrapper)) {
                    Map<String, CryptField> cachedMethodMqPlaceHolders = this.metadataCacheManager.getCachedMethodMqPlaceHolders(wrapper, metadata, boundSql, configuration);
                    this.encryptParam(cachedMethodMqPlaceHolders, parameterObject[0]);
                }
                HashMap<String, Object> hashMap = (HashMap<String, Object>) parameterObject[0];
                if (hashMap.containsKey(Constants.ENTITY) && (hashMap.get(Constants.ENTITY) == null)) {
                    return;

                }
            }
            MethodCryptMetadata metaData = this.metadataCacheManager.getCachedMethodCryptMetaData(metadata.getDaoStatementId(), metadata.getDaoRunningMethod(), parameterObject[0]);
            ParameterCls parameterCls = buildEncryptObject(boundSql, parameterObject[0], metaData);

            //region 重新解析参数
            parameterCls.parameterObject = parserParam(configuration, metadata.getDaoRunningMethod(), parameterCls.parameterObject);
            //endregion

            if (parameterHandler instanceof MybatisParameterHandler) {
                try {
                    Field mappedStatementField = invocationCls.mappedStatementField;
                    MappedStatement ms = (MappedStatement) mappedStatementField.get(parameterHandler);
                    BoundSql tempBoundSql = ms.getBoundSql(parameterCls.parameterObject);
                    PluginUtils.MPBoundSql tempB = PluginUtils.mpBoundSql(tempBoundSql);
                    PluginUtils.setAdditionalParameter(boundSql, tempB.additionalParameters());
                } catch (Exception e) {
                }
            }
//            }
            try {
                parameterField.set(parameterHandler, parameterCls.parameterObject);
                parameterHandler.setParameters(ps);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
        return invocation.proceed();
    }

    private Object parserParam(Configuration configuration, Method daoRunningMethod, Object args) {
        ParamNameResolver paramNameResolver = new ParamNameResolver(configuration, daoRunningMethod);
        if (args instanceof Map) {
            Map<String, Object> tempMap = (Map<String, Object>) args;
            String[] names = paramNameResolver.getNames();
            ArrayList<Object> params = new ArrayList<>(names.length);
            for (String name : names) {
                Object o = tempMap.get(name);
                params.add(o);
            }
            return paramNameResolver.getNamedParams(params.toArray());
        }
        return args;
    }

    private InvocationCls buildInvokeInfo(ParameterHandler parameterHandler) throws NoSuchFieldException {
        Class<? extends ParameterHandler> parameterHandlerClass = parameterHandler.getClass();
        InvocationCls invocationCls = new InvocationCls();
        //region parameterObject
        Field parameterField = parameterHandlerClass.getDeclaredField("parameterObject");
        if (!parameterField.isAccessible()) {
            parameterField.setAccessible(true);
        }
        invocationCls.parameterObjectField = parameterField;
        //endregion

        //region BoundSql
        Field boundSqlField = parameterHandlerClass.getDeclaredField("boundSql");
        if (!boundSqlField.isAccessible()) {
            boundSqlField.setAccessible(true);
        }
        invocationCls.boundSqlField = boundSqlField;
        //endregion

        //region typeHandlerRegistry
        Field typeHandlerRegistryField = parameterHandlerClass.getDeclaredField("typeHandlerRegistry");
        if (!typeHandlerRegistryField.isAccessible()) {
            typeHandlerRegistryField.setAccessible(true);
        }
        invocationCls.typeHandlerRegistryField = typeHandlerRegistryField;
        //endregion

        //region configuration
        Field configurationField = parameterHandlerClass.getDeclaredField("configuration");
        if (!configurationField.isAccessible()) {
            configurationField.setAccessible(true);
        }
        invocationCls.configurationField = configurationField;
        //endregion

        //region mappedStatement
        Field mappedStatementField = parameterHandlerClass.getDeclaredField("mappedStatement");
        if (!mappedStatementField.isAccessible()) {
            mappedStatementField.setAccessible(true);
        }
        invocationCls.mappedStatementField = mappedStatementField;
        //endregion

        return invocationCls;
    }


    private ParameterCls buildEncryptObject(BoundSql boundSql,
                                            Object parameterObject,
                                            MethodCryptMetadata metaData) {
        Object encrypt = metaData.encrypt(parameterObject);
        ParameterCls parameterCls = new ParameterCls();
        parameterCls.parameterObject = encrypt;
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (!CollectionUtils.isEmpty(parameterMappings)) {
            ParameterMapping parameterMapping = parameterMappings.get(0);
            if (parameterMapping.getMode() != ParameterMode.OUT) {
                String propertyName = parameterMapping.getProperty();
                if (boundSql.hasAdditionalParameter(propertyName)) {
                    parameterCls.isIterable = true;
                }
            }
        }
        return parameterCls;
    }

    //region cls
    private static class ParameterCls {
        /**
         * 是否是可迭代的
         */
        private boolean isIterable = false;
        private Object parameterObject;
    }

    private static class InvocationCls {
        private Field parameterObjectField;
        private Field boundSqlField;
        private Field typeHandlerRegistryField;
        private Field configurationField;
        private Field mappedStatementField;
    }
    //endregion


    private List<Object> getParameters(BoundSql boundSql,
                                       Object parameterObject,
                                       TypeHandlerRegistry typeHandlerRegistry,
                                       Configuration configuration) {
        List<Object> oldPropertyValues = null;
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings != null) {
            oldPropertyValues = new ArrayList<>(parameterMappings.size());
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    Object value;
                    String propertyName = parameterMapping.getProperty();
                    if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if (parameterObject == null) {
                        value = null;
                    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        value = parameterObject;
                    } else {
                        MetaObject metaObject = configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }
                    oldPropertyValues.add(value);
                }
            }
        }
        return oldPropertyValues;
    }

    private void checkLikeSql(BoundSql boundSql,
                              Object parameterObject,
                              TypeHandlerRegistry typeHandlerRegistry,
                              Configuration configuration, Consumer<List<Object>> consumer) {
        List<Object> oldParameters = getParameters(boundSql, parameterObject, typeHandlerRegistry, configuration);
        consumer.accept(oldParameters);
        List<Object> newParameters = getParameters(boundSql, parameterObject, typeHandlerRegistry, configuration);
        if (Objects.equals(oldParameters, newParameters)) {
            return;
        }
        if (CollectionUtils.isEmpty(oldParameters) || CollectionUtils.isEmpty(newParameters)) {
            return;
        }
        //预编译的sql
        String sql = boundSql.getSql();
        for (int i = oldParameters.size() - 1; i >= 0; i--) {
            int maskIndex = sql.lastIndexOf(ConstStringPool.QUESTION_MARK);
            sql = sql.substring(0, maskIndex);
            if (!Objects.equals(oldParameters.get(i), newParameters.get(i))) {
                if (sql.trim().toLowerCase(Locale.SIMPLIFIED_CHINESE).endsWith(ConstStringPool.LIKE_LOWERCASE)) {
                    throw new ReflectionException("【" + sql + "】最后一个字段为加密字段，暂不支持like语句");
                }
            }
        }
    }


    /**
     * 在paramObject里加密数据
     *
     * @param mqValuePlaceholders mybatisplus的值占位符
     * @param paramObject         参数对象
     */
    @SuppressWarnings("all")
    private void encryptParam(Map<String, CryptField> mqValuePlaceholders, Object paramObject) {
        if (CollectionUtils.isEmpty(mqValuePlaceholders)) {
            return;
        }
        AbstractWrapper abstractWrapper = (AbstractWrapper) ((MapperMethod.ParamMap) paramObject).get(Constants.WRAPPER);
        //checkLikeSql(mqValuePlaceholders, abstractWrapper);
        Map ew = abstractWrapper.getParamNameValuePairs();
        for (Map.Entry<String, CryptField> mqValuePlaceholder : mqValuePlaceholders.entrySet()) {
            String oldVal = String.valueOf(ew.get(mqValuePlaceholder.getKey()));
            ew.put(mqValuePlaceholder.getKey(), CryptExecutorFactory.getTypeHandler(mqValuePlaceholder.getValue()).encrypt(oldVal));
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // document why this method is empty
    }

    private boolean isSwitchCrypt() {
        return this.dataSecurityDbCryptProperty.isEnabled();
    }
}
